安卓面试题(19/30)View绘制流程全解析

您所在的位置:网站首页 android view绘制流程面试题 安卓面试题(19/30)View绘制流程全解析

安卓面试题(19/30)View绘制流程全解析

2024-07-17 12:06| 来源: 网络整理| 查看: 265

安卓面试题(19/30)View绘制流程全解析 牛客高级系列专栏: 安卓(安卓系统开发也要掌握) 想通关安卓面试,请看(承诺免费售后答疑):《150道安卓高频面试题目录及答案链接》 想通关安卓系统面试,请看:《140道安卓系统Framework面试题目录及答案链接》 想进阶安卓开发,请看(承诺免费售后答疑):《Android进阶知识体系解析_15大安卓进阶必备知识点》 想了解安卓APP完整开发流程,请看(承诺免费售后答疑):《安卓APP完整开发流程》 想掌握安卓App性能优化,请看(承诺免费售后答疑):《安卓性能优化讲解和实战专栏》 想掌握Gradle语法和配置,制作Gradle插件,请看(承诺免费售后答疑):《安卓Gradle语法解析和实践大全》 嵌入式 想通关嵌入式面试,请看: 《111道嵌入式面试题目录及答案链接》 想多掌握几个嵌入式项目,请看:《6个嵌入式项目交流分享(附源码)》

本人是2020年毕业于广东工业大学研究生:许乔丹,有国内大厂CVTE和世界500强企业安卓开发经验,该专栏整理本人对常见安卓高频开发面试题的理解;

网上安卓资料千千万,笔者将继续维护专栏,一杯奶茶价格不止提供答案解析,承诺提供专栏内容免费技术答疑,直接咨询即可。助您提高安卓面试准备效率,为您面试保驾护航!

正文开始⬇

View的绘制流程是Android开发的核心知识,也是实现自定义View的基础知识。我们看看面试官可能会问什么吧:

请介绍View的三大绘制流程 ⭐⭐⭐⭐⭐ 你知道View绘制前的准备流程吗?⭐ 什么是MeasureSpec?⭐⭐⭐ 测量模式有哪三种?⭐⭐ 为什么有时候getMeasuredWidth获取值为0?⭐⭐ 绘制的顺序是怎么样?⭐⭐

其实上面关于View绘制相关的面试题,估计面试官就只问View的三大绘制流程。如果可以简单的叙述出来,应该就过关了。然后面试官就会问你自定义View相关的问题,自定义View会另外写一篇。至于其他问题,比较细,较少有面试官会问,但还是需要过一遍整体的流程:

看完以下的解析,一定可以让面试官眼前一亮。

目录 1、View绘制前的流程 2、View绘制 - Measure(测量) 2.1 Measure源码流程 2.2 什么是MeasureSpec 2.2.1 测量模式 2.2.2 MeasureSpec如何确定 2.3 ViewGroup的测量 2.4 Measure总结 3、View绘制 - Layout(布局) 3.1 Layout源码流程 3.2 LinearLayout的onLayout() 3.3 Layout流程图 4、View绘制 - Draw(绘制) 4.1 Draw源码流程 4.2 Draw的步骤 4.3 Draw的流程图 5、总结 1、View绘制前的流程

入门一段时间的同学都会知道,View绘制流程就是Measure() -> Laytou() -> Draw(),这三大方法。没错,不过在进行这三个方法之前,系统又做了什么准备工作呢?这就是本小节解决的问题。以下的图,是贯穿全文的总结图,放在文章开头,我们一步一步分析。

alt

以下内容与本系列《Activity、Window、DecorView以及ViewRoot层级关系》(以下简称为《参考文档1》)一文内容重复较多,特别是源码分析,故具体源码请看参考文档1。从Activity创建到View的绘制,经过以下步骤:

Step1:初始化PhoneWindow和WindowManager

之前一节说过,Activity 是在 ActivityThread 的 performLaunchActivity 中进行创建的,并在attach()方法中创建了PhoneWindow,并初始化WindowManager。attach()方法详情见参考文档1的第2小节。

Step2:初始化DecorView

系统执行完attach()方法后,会执行onCreate()方法,在onCreate()方法中执行setContentView()方法将布局xml文件解析为View并设置到DecorView里面的ContentViews控件。这里的源码也对应参考文档1的第2小节。

Step3:ViewRootImpl创建和关联DecorView

接着在handleResumeActivity()方法中,通过WindowManager的addView()方法将DecorView添加到WindoManager里面。通过源码分析,WindowManager的addView()方法最终通过WindowManagerGlobal的实例去addView(),在 WindowManagerGlobal()方法中,会创建一个ViewRootImpl,也就是最后会把DecorView传给了ViewRootImpl中的setView()方法。ViewRootImpl是DecorView的管理者,是WindowManager与DecorView之间的连接器。源码详情见参考文档1的第4小节。

Step4:建立 PhoneWindow 和 WindowManagerService 之间的连接

WindowManagerService是所有Window窗口的管理者,负责Window的添加删除、事件派发等。每一个Activity都有一个PhoneWindow对象,操作PhoneWindow对象进行显示等操作,都需要和WindowManagerService进行交互。在上面第三步中的ViewRootImpl的setView()方法中,会执行setView()方法,会调用 requestLayout()方法,并且通过 WindowSession 的 addToDisplay()方法 与 WindowManagerService 进行交互。WindowManagerService 会为每一个 Window 关联一个 WindowStatus。到此,我们已经把DecorView加载到Window中了。

Step5:建立与SurfaceFlinger的链接

SurfaceFlinger主要对图层数据进行合成,然后发送到屏幕渲染。在第4步中,WindowStatus会创建一个SurfaceSession,SurfaceSession 会在 Native 层构造一个 SurfaceComposerClient 对象,它是应用程序与 SurfaceFlinger 沟通的桥梁。

Step6:申请Surface

View绘制会从ViewRoot的performTraversals(),按照Measure() -> Layout() -> Draw()经典流程完成View绘制。不过在此之前还会执行一个重要的函数relayoutWindow()。代码如下:

//frameworks/base/core/java/android/view/ViewRootImpl.java private void performTraversals() { relayoutResult = relayoutWindow(params, viewVisibility, insetsPending); //1 ... int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); ... //执行测量流程 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //2 ... //执行布局流程 performLayout(lp, desiredWindowWidth, desiredWindowHeight); //3 ... //执行绘制流程 performDraw(); //4 }

调用[注释1]的relayoutWindow()方法,通过 WindowSession与WindowManagerService交互,即把Java层的Surface和Native层的Surface关联在一起。

Step7:正式进入View绘制

接下来就是正式绘制View的整体流程,即[注释2-4]三步走。绘制会根视图ViewRoot的performTraversals()方法开始,从上到下遍历整个视图树,每个ViewGroup会负责通知自己的子View进行绘制,而每个子View控件则负责绘制自己。大致的工作流程图如下:

alt

从上面的图,可以看到有onMeasure(),onLayout(),onDraw()这三个函数,是我们在实现自定义View最常接触的三个函数,接下来以这三个函数为思路进行讲解。

2、View绘制 - Measure(测量) 2.1 Measure源码流程

Measure翻译过来即是“测量”的意思,在此测量的是每个控件的宽和高。在代码层面,则是给每个View的mMeasuredWidth和mMeasuredHeight变量进行赋值。在测量时遵循:

如果是ViewGroup,则遍历测量子View的宽高,再根据子View宽高算出自身的宽高; 如果是子View,则直接测量出自身宽高;

现在从[注释2]的performMeasure()方法开始:

//frameworks/base/core/java/android/view/ViewRootImpl.java private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { if (mView == null) { return; } Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); //5 } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } }

逻辑很清晰,可发现实际起作用的是[注释5]mView.measure()方法,

//frameworks/base/core/java/android/view/View.java public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... if (forceLayout || needsLayout) { if (cacheIndex < 0 || sIgnoreMeasureCache) { onMeasure(widthMeasureSpec, heightMeasureSpec); //6 } else { setMeasuredDimensionRaw((int) (value >> 32), (int) value); } } ... }

measure()方法使用final修饰,代表不可重写。在measure()方法中会进行一系列逻辑处理后,调用[注释6]的onMeasure()方法,真正的测量都在onMeasure()方法中实现。

//frameworks/base/core/java/android/view/View.java protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); //7 }

可以看到onMeasure()方法使用protected修饰,代表我们可以重写该方法。因此如果需要实现自己的测量逻辑,只能通过子View重写onMeasure()方法,而不能重写measure()方法。onMeasure()最后调用[注释7]setMeasuredDimension()设置View的宽高信息,完成View的测量操作。

看看getDefaultSize()的源码:

public static int getDefaultSize(int size, int measureSpec) { int result = size; //通过MeasureSpec解析获取mode与size int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: //8 case MeasureSpec.EXACTLY: //8 result = specSize; break; } return result; }

这是系统设置默认的尺寸,在[注释8]可以看到如果specMode是AT_MOST或者EXACTLY,则返回的就是specSize。至于 UNSPECIFIED 的情况,则会返回一个建议的最小值,这个值和子元素设置的最小值它的背景大小有关(这一段话可先看看2.2小节再回来继续看)。

从一开始执行的performMeasure()到最后设置宽高的setMeasuredDimension()方法,流程都比较清晰。并且可以发现有两个贯穿整个流程的变量,widthMeasureSpec和heightMeasureSpec,理解这两个变量才是关键。

2.2 什么是MeasureSpec

MeasureSpec是一个32位的int型数据,由两部分组成,SpecMode(测量模式,高2位) + SpecSize(测量尺寸,低30位)。将这两者打包为一个int数据可以起到节省内存的作用。有打包当然也有解包的方法:

//获取测量模式 public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } //获取测量尺寸 public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); }

名词解析:控件的布局参数LayoutParams是指控件设定为match_parent或者wrap_content或具体数值之中的一种。

2.2.1 测量模式 EXACTLY:确定大小,父View希望子View的大小是确定的。对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值; AT_MOST :最大大小,父View希望子View的大小最多是specSize指定的值。对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。 UNSPECIFIED :不确定大小,父View完全依据子View的设计值来决定。系统不对View进行任何限制,要多大给多大,一般用于系统内部。

具体详见2.2.2小节的图。

2.2.2 MeasureSpec如何确定 DecorView:通过屏幕大小和自身布局参数LayoutParams,只要将自身大小和屏幕大小相比,设置一个不超过屏幕大小的宽高和对应测量模式即可; ViewGroup和View:需要通过父布局的MeasureSpec和自身的布局参数LayoutParams确定,具体如下:

alt

2.3 ViewGroup的测量

上面说过ViewGroup需要测量其包含的子View的宽高后,根据子View宽高算出自身的宽高。所以在ViewGroup中定义了measureChildren(), measureChild(), measureChildWithMargins()方法来对子视图进行测量,measureChildren()内部实质只是循环调用measureChild()。

//frameworks/base/core/java/android/view/ViewGroup.java protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); //调用measureChild()对子View进行测量 } } } protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { //获取子视图的LayoutParams final LayoutParams lp = child.getLayoutParams(); //通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSp

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

Android高频面试题全解析 文章被收录于专栏

#提供免费售后答疑!!花一杯奶茶的钱获得安卓面试答疑服务,稳赚不赔# Android发展已经很多年,安卓资料网上千千万,本专栏免费提供专栏内容技术答疑!!私聊当天必回。在阅读过程或者其他安卓学习过程有疑问,都非常欢迎私聊交流。

提示


【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3